#!/usr/bin/env python
# coding: utf-8


'''
等额本息，按日计息公式推导：

Qn 每期还款结束剩余本金
P 本金
r 日利率
R 月利率
Dn 每期实际天数
X 每期等额本息，目标

Q1 = P * ( 1 + D1 * r ) - X
Q2 = Q1 * ( 1 + D2 * r) - X
   = P * ( 1 + D1 * r ) * ( 1 + D2 * r) - ( 1 + D2 * r) * X - X
Q3 = Q2 * ( 1 + D3 * r) - X
   = P * ( 1 + D1 * r ) * ( 1 + D2 * r) * ( 1 + D3 * r) - ( 1 + D2 * r) * ( 1 + D3 * r) * X - ( 1 + D3 * r) * X - X
Q4 = Q3 * ( 1 + D4 * r) - X
   = P * ( 1 + D1 * r ) * ( 1 + D2 * r) * ( 1 + D3 * r) * ( 1 + D4 * r) - ( 1 + D2 * r) * ( 1 + D3 * r) * ( 1 + D4 * r) * X - ( 1 + D3 * r) * ( 1 + D4 * r) * X - ( 1 + D4 * r) * X - X

等额本息，按月计息，从按日计息公式推导：

if D1 * r = D2 * r = D3 * r = D4 * r = R





Qn 每期还款结束剩余本金
P 本金
r 日利率
R 月利率
Dn 每期实际天数
X 每期等额本息，目标

Q1 = P * ( 1 + D1 * r ) - X
Q2 = Q1 * ( 1 + D2 * r) - X
   = P * ( 1 + D1 * r ) * ( 1 + D2 * r) - ( 1 + D2 * r) * X - X
Q3 = Q2 * ( 1 + D3 * r) - X
   = P * ( 1 + D1 * r ) * ( 1 + D2 * r) * ( 1 + D3 * r) - ( 1 + D2 * r) * ( 1 + D3 * r) * X - ( 1 + D3 * r) * X - X
Q4 = Q3 * ( 1 + D4 * r) - X
   = P * ( 1 + D1 * r ) * ( 1 + D2 * r) * ( 1 + D3 * r) * ( 1 + D4 * r) - ( 1 + D2 * r) * ( 1 + D3 * r) * ( 1 + D4 * r) * X - ( 1 + D3 * r) * ( 1 + D4 * r) * X - ( 1 + D4 * r) * X - X

Qn = P * ( 1 + D1 * r ) * ( 1 + D2 * r) * ( 1 + D3 * r) * ... * ( 1 + Dn * r) - ( 1 + D2 * r) * ( 1 + D3 * r) * ... * ( 1 + Dn * r) * X - ( 1 + D3 * r) * ... * ( 1 + Dn * r) * X - ... - ( 1 + Dn * r) * X - X = 0

P * ( 1 + D1 * r ) * ( 1 + D2 * r) * ( 1 + D3 * r) * ... * ( 1 + Dn * r) = ( 1 + D2 * r) * ( 1 + D3 * r) * ... * ( 1 + Dn * r) * X + ( 1 + D3 * r) * ... * ( 1 + Dn * r) * X + ... + ( 1 + Dn * r) * X + X

X = (P * ( 1 + D1 * r ) * ( 1 + D2 * r) * ( 1 + D3 * r) * ... * ( 1 + Dn * r)) / (( 1 + D2 * r) * ( 1 + D3 * r) * ... * ( 1 + Dn * r) + ( 1 + D3 * r) * ... * ( 1 + Dn * r) + ... + ( 1 + Dn * r) + 1)


if D1 = D2 = D3 = D4
then R = D * r


分期除不尽会有尾差
分钱（按科目分、按出资比例分）除不尽会有尾差
分钱（按科目分、按出资比例分）除的尽也可能有，拆分后，各自四舍五入后相加的值和拆分前的值可能不一致，产生尾差
所以要严格规定保留小数位数和保留小数的方式，并设计补齐尾差的方式



金额精度分
年利率百分
月利率千分
日利率万分，一厘一毫，角、分、厘、毫

逾期预算
还款预算
结清预算
提前还款



大本大息
小本小息

（一）贷款未逾期的，按照先还利息、后还本金的顺序依次偿还；

（二）贷款逾期的，按照先还逾期部分、后还非逾期部分的顺序依次偿还。对于逾期贷款部分，按照逾期时间的先后顺序、逐期偿还；对于同一期逾期贷款，按照罚息、应还利息、贷款本金的顺序依次偿还。
用列表存储顺序


'''


'''
起息日...还款日|起息日...还款日|起息日...还款日|...
起息日...还款日|...还款日|...还款日|...
期次n持有时间 = 期次n还款日 - 期次n-1还款日 
期次n持有时间 = 期次n还款日 - 期次n起息日
其次1持有时间 < ... 需合并期次，装饰器模式，模板模式都可以实现 
'''


'''
金额类型：等额本息每期应还金额
保留小数：2
保留方式：向下取整
'''

import math
import calendar
import logging
from datetime import datetime, timedelta
from functools import lru_cache
from decimal import Decimal
import uuid

 
def merge_date(tag, x=15, y=15):
    #标签
    #头长
    #尾长
    def wrapper(func):
        def sub_wrapper(*args, **kwargs):
            dates=func(*args, **kwargs)
            if tag == "fixed_day":
                if len(dates) > 1 and (dates[1] - dates[0]).days + 1 < x:              
                    dates.pop(1)
            return dates
        return sub_wrapper
    return wrapper

def process_amount(tag, decimal, direction):
    #金额类型，暂时就做个标记，保留小数位数+保留小数方式
    #保留小数位数：保留小数位数
    #保留小数位数：ceil向上取整、round四舍五入、floor向下取整、cut截断
    def wrapper(func):
        def sub_wrapper(*args, **kwargs):
            amount=func(*args, **kwargs)
            scaler = 10 ** decimal
            amountScaled = amount * scaler
            if direction == "ceil": amountScaled = math.ceil(amountScaled) 
            if direction == "floor": amountScaled = math.floor(amountScaled)
            if direction == "cut": amountScaled = int(amountScaled) 
            if direction == "round": amountScaled = round(amountScaled)
            if direction == "bare": pass
            return amountScaled / scaler
        return sub_wrapper
    return wrapper
    


# class Installment:
#     #期次
#     term = 0
#     #总期次 
#     terms = 0
#     #当前期次 
#     now = 0
#     #期初应还本金
#     contract_principal_accrued_initial  = ""
#     #期末应还本金
#     contract_principal_accrued_closing  = ""
#     #本期应还利息
#     installment_interest_accrued  = ""
#     #本期已还利息
#     installment_interest_cleared = ""
#     #本期剩余利息
#     installment_interest_torepay = ""
#     #本期期逾期利_
#     installment_interest_overdue = ""
#     #本期应还本金
#     installment_principal_accrued = ""
#     #本期已还本金
#     installment_principal_cleared = ""
#     #本期剩余本金
#     installment_principal_torepay = ""
#     #本期逾期本金
#     installment_principal_overdue = ""
#     #本期利息罚息
#     installment_interest_penalty = ""
#     #本期本金罚息
#     installment_principal_penalty = "" 
#     #是否过期
#     overdue = 0
#     #是否结清
#     clear = 0
#     #是否应计
#     accrued = 1
#     #是否核销
#     cancel = 0
#     @classmethod
#     def plan(cls, a1, a2, a3, a4):
#         installment = cls()
#         #期初应还本金
#         installment.contract_principal_accrued_initial = a1
#         #本期应还利息
#         installment.installment_interest_accrued = a2
#         #本期应还本金
#         installment.installment_principal_accrued = a3
#         #期末应还本金
#         installment,contract_principal_accrued_closing = a4
#         return installment




class Loan000:

    CORPUS_FIXEDDAY_DAILY = "{repayModelNo}{termModelNo}{rateModelNo}".format(repayModelNo=0, termModelNo=0, rateModelNo=0)

    def __init__(self, principal, terms, yearStart, monthStart, dayStart, dayRepay, rateDaily):
        
        #业务无关主键
        self.uuid = uuid.uuid1()
        self.created = ""
        self.updated = ""
        
        #贷款模型
        self.loanModelNo="000" #贷款模型编号
        self.principal=principal #数据项，本金
        self.terms=terms #数据项，总期次
        self.yearStart=yearStart #数据项，开始年
        self.monthStart=monthStart #数据项，开始月
        self.dayStart=dayStart #数据项，起息日

        #贷款属性之还款模式
        self.repayModelNo="0"
        self.installment_corpus_accrued = 0 #支持等额本息还款，该还款模式计算项，每期金额
        self.installment_principal_accrued = 0 #支持等额本金还款，该还款模式计算项，每期本金

        
        #贷款属性之还款时间节点
        self.termModelNo="0"
        self.dates = [] #还款时间节点通用计算项，起息日,还款日,还款日
        self.termDays = [] #还款时间节点通用计算项，每期首尾左闭右开，每期天数算头不算尾
        self.dayRepay= dayRepay #支持固定日期还款，该还款时间节点参数项，固定还款日
        self.intervalDays = 30 #支持固定间隔还款，该还款时间节点参数项，固定还款间隔
        
        #贷款属性之利息粒度
        self.rateModelNo="0"
        self.rateDaily = rateDaily #支持日利率粒度计息，该利息粒度参数项，日利率
        self.rateMonthly = 0.0004 * 30 #支持月利率粒度计息，该利息粒度参数项，月利率

        #待拆分贷款父类和子类，当前类应为子类，等额本息，固定还款日还款，按日计息

        #初始化还款时间节点，起息日、还款日
        self.calculate_dates_fixed_day()
        #初始化每期天数，每期首尾左闭右开，每期天数算头不算尾
        self.calculate_termdays()
        #初始化等额本息每期应还金额
        self.installment_corpus_accrued = self.caculate_installment_corpus_accrued_by_day_rate()

        print(
            "主键：{uuid}\n本金：{principal}\n贷款期次：{terms}\n起息日：{yearStart}/{monthStart}/{yearStart}\n每月固定还款日：{dayRepay}".format(
                uuid=self.uuid,
                principal=principal,
                terms=terms, 
                yearStart=yearStart, 
                monthStart=monthStart, 
                dayStart=dayStart, 
                dayRepay=dayRepay
            )
        )
        
        for i in range(len(self.termDays)):
            print(
                "左闭右开区间，每期首尾左闭右开，每期天数算头不算尾：{0} {1}：{2}".format(
                    self.dates[i].strftime("%Y-%m-%d"), 
                    self.dates[i+1].strftime("%Y-%m-%d"), 
                    self.termDays[i]
                )
            )
        print("确定起息日、还款日：\n{0}".format([date.strftime("%Y-%m-%d") for date in self.dates]))
        print("确定每期天数（算头不算尾）：\n{0}".format(self.termDays))
        print("确定等额本息每期应还金额：{0}".format(self.installment_corpus_accrued))


        #初始化划款计划
        self.initRepayment(self.terms)



    def previewRepayment(self):
        pass 
        
    #常用金额
    def printNouns(self):
        #常用金额参数
        #terms
        #rateDaily
        #rateMonthly
        #rateAnnual
        #rateMultiplier
        #常用金额变量
        for amountGranularity in ['installment', 'contract']:
            for amountType in ['principal', 'interest', 'corpus', 'fee', 'charge', 'amount']:
                for amountState in ['accrued', 'overdue', 'nonaccrual', 'cancel', 'prepay', 'repay', 'penalty']:
                    amountItem = "{0}_{1}_{2}".format(amountGranularity, amountType, amountState)
                    print(amountItem)
                     
    #固定日期还款，当该月该不存在该日期时，以月末为还款日
    def calculate_repay_date(self, year, month, day):
        weekday, monthday = calendar.monthrange(year, month)
        return datetime(year, month, monthday) if  day > monthday else datetime(year, month, day)
        
    #等额本息，按日计息
    def calculate_bottom(self):
        #注意，每期首尾左闭右开，每期天数算头不算尾
        
        termDays = self.termDays
        rateDaily = self.rateDaily
        
        n = len(termDays)
        if n > 1:
            elements = [1 + termDay * rateDaily for termDay in termDays]
            base = elements.copy()
            for term in range(n, 2, -1):
                base = [ val * elements[term-1] for val in base[:term-1] ] + base[term-1:]
            print("BOTTOM {0}".format(sum(base[1:]) + 1))
            return sum(base[1:]) + 1

        return 0

    def calculate_top(self):
        #注意，每期首尾左闭右开，每期天数算头不算尾
        
        termDays = self.termDays
        rateDaily = self.rateDaily
        
        n = len(termDays)
        base = [1 + termDay * rateDaily for termDay in termDays]
        for i in range(1, n):
            base[0] = base[0] * base[i]

        print("TOP {0}".format(base[0]))
        return base[0]
    
    #固定日期还款
    #假设到期日和还款日是同一天
    #是否合并首期
    #返回[起息日,还款日,还款日,]
    @merge_date("fixed_day")
    def calculate_dates_fixed_day(self):
    
        terms = self.terms
        yearStart = self.yearStart
        monthStart = self.monthStart
        dayStart = self.dayStart
        dayRepay = self.dayRepay
      
        #计算最后一年
        if (monthStart + terms) % 12  > 0:
            yearEnd = yearStart + (monthStart + terms) // 12
        if (monthStart + terms) % 12  == 0:
            yearEnd = yearStart + (monthStart + terms) // 12 - 1
        #计算最后一年的最后一个月
        monthEnd =  (monthStart + terms) % 12
        monthEnd = monthEnd if monthEnd else 12

        #第一年的所有月+第一年和最后一年间存在完整的年+最后一年的所有月
        #第一年的所有月
        dates = [ 
            datetime(yearStart, month, self.calculate_repay_date(yearStart, month, dayRepay).day) 
            for month in range(monthStart, 12 + 1) 
            if month < monthStart + terms + 1
        ]
        #第一年和最后一年间存在完整的年，可能没有
        for year in range(yearStart+1, yearEnd):
            dates += [ datetime(year, month, calculate_repay_date(year, month, dayRepay).day) for month in range(1, 12 + 1) ]
        #最后一年的所有月，可能没有
        if yearEnd > yearStart:
            dates += [ datetime(yearEnd, month, calculate_repay_date(yearEnd, month, dayRepay).day) for month in range(1, monthEnd + 1) ]
        #起息日+所有还款日
        dates = [ datetime(yearStart, monthStart, dayStart) ] + dates
        self.dates = dates
        return dates
        

    #固定间隔还款
    #假设到期日和还款日是同一天
    #是否合并首期
    #返回[起息日,还款日,还款日,]
    def calculate_dates_fixed_interval(self):
    
        terms = self.terms
        yearStart = self.yearStart
        monthStart = self.monthStart
        dayStart = self.dayStart
        intervalDays = self.intervalDays
        
        dateStart = datetime(yearStart, monthStart, dayStart)
        dates = [  dateStart + timedelta(days=i*intervalDays) for i in range(1, terms+1) ]
        dates = [ dateStart ] + dates
        self.dates = dates
        return dates

    #每期首尾左闭右开，每期天数算头不算尾
    def calculate_termdays(self):  
        dates = self.dates
        termDays = [ (dates[i] - dates[i-1]).days for i in range(1, len(dates)) ]
        self.termDays = termDays
        return termDays
        
    @process_amount(tag="installment_corpus_accrued", decimal=2, direction="floor")
    def caculate_installment_corpus_accrued_by_day_rate(self):
        principal = self.principal
        rateDaily = self.rateDaily
        dates = self.dates
        print("TOP * PRINCIPLE {0}".format(principal * self.calculate_top()))
        installment_corpus_accrued = principal * self.calculate_top() / self.calculate_bottom()
        return installment_corpus_accrued
    
    @process_amount("installment_interest_accrued", 2, "floor")
    def calculate_interest_each_term(self, principal, days, rateDaily):
        #本期应还利息
        return principal * days * rateDaily
    
    @process_amount("installment_principle_accrued", 2, "bare")
    def calculate_principal_each_term(self, corpus, interest):
        #本期应还本金
        return corpus - interest

    def minus(self, x, y):
        x = Decimal(str(x))
        y = Decimal(str(y))
        return float(x - y)

        
    def caculate_installment_corpus_accrued_by_month_rate(self):
        principal = self.principal
        terms = self.terms
        rateMonthly = self.rateMonthly
        installment_corpus_accrued = principal * rateMonthly * ( 1 + rateMonthly ) ** terms / ( ( 1 + rateMonthly ) ** terms - 1 )
        self.installment_corpus_accrued = installment_corpus_accrued
        return installment_corpus_accrued
    
    
        
    
    @lru_cache()
    def initRepayment(self, term):
    
        principal = self.principal
        rateDaily = self.rateDaily
        dates = self.dates
        termDays = self.termDays
        installment_corpus_accrued = self.installment_corpus_accrued
        
        contract_principal_accrued_initial = 0
        installment_interest_accrued = 0
        installment_principal_accrued = 0
        contract_principal_accrued_closing = 0
              
        #首期，递归转折点
        if term == 1:
            #期初应还本金
            contract_principal_accrued_initial = principal 
            #本期应还利息
            installment_interest_accrued = self.calculate_interest_each_term(contract_principal_accrued_initial, termDays[term-1], rateDaily)
            #本期应还本金
            installment_principal_accrued = self.calculate_principal_each_term(self.installment_corpus_accrued, installment_interest_accrued)
            #期末应还本金
            contract_principal_accrued_closing = self.minus(contract_principal_accrued_initial, installment_principal_accrued)

        #在计算等额本息每期应还金额时，结果向下取整，在分期的最后一期避免尾差的产生
        if term == self.terms:
            #期初应还本金
            contract_principal_accrued_initial = self.initRepayment(term-1)["contract_principal_accrued_closing"]
            #本期应还利息
            installment_interest_accrued = self.calculate_interest_each_term(contract_principal_accrued_initial, termDays[term-1], rateDaily)
            #本期应还本金
            installment_principal_accrued = contract_principal_accrued_initial
            #期末应还本金
            contract_principal_accrued_closing = 0
        
        #非首期、非尾期
        if term != 1 and term != self.terms:
            #期初应还本金
            contract_principal_accrued_initial = self.initRepayment(term-1)["contract_principal_accrued_closing"]
            #本期应还利息
            installment_interest_accrued = self.calculate_interest_each_term(contract_principal_accrued_initial, termDays[term-1], rateDaily)
            #本期应还本金
            installment_principal_accrued = self.calculate_principal_each_term(self.installment_corpus_accrued, installment_interest_accrued)
            #期末应还本金
            contract_principal_accrued_closing = self.minus(contract_principal_accrued_initial, installment_principal_accrued)

        print(
            "期次：{0}\n本期应还利息：{1}\n本期应还本金：{2}\n期初应还本金：{3}\n期末应还本金：{4}".format(
                term, 
                installment_interest_accrued, 
                installment_principal_accrued,
                contract_principal_accrued_initial,
                contract_principal_accrued_closing
            )
        )

        amounts = {
            'uuid': self.uuid,
            'terms': self.terms,
            'term': term,
            'contract_principal_accrued_initial': contract_principal_accrued_initial,
            'contract_principal_accrued_closing': contract_principal_accrued_closing,
            'installment_corpus_accrued': installment_corpus_accrued, 
            'installment_principal_accrued': installment_principal_accrued, 
            'installment_interest_accrued': installment_interest_accrued 
        }
        
        return amounts
    
    def caculate_replay_plan_by_month_rate(self, n, terms, principal, rateMonthly):
  
        if n == 1:
        
            plan = []
            
            last_contract_principal_accrued = 0
            installment_corpus_accrued = caculate_installment_corpus_accrued(principal, terms, rateMonthly)
            installment_interest_accrued = principal * 1 * rateMonthly
            installment_principal_accrued = installment_corpus_accrued - installment_interest_accrued
            contract_principal_accrued = last_contract_principal_accrued  + installment_principal_accrued
            contract_interest_accrued = installment_corpus_accrued * n - contract_principal_accrued
            #contract_principal_accrued = int( contract_principal_accrued * amountPrecision ) / amountPrecision
            
            
            #amounts = [ 
            #  contract_principal_accrued, 
            #  contract_interest_accrued, 
            #  installment_corpus_accrued, 
            #  installment_principal_accrued, 
            #  installment_interest_accrued 
            #]    
            amounts = {
              'term': n,
              'contract_principal_accrued': contract_principal_accrued, 
              'contract_interest_accrued': contract_interest_accrued, 
              'installment_corpus_accrued': installment_corpus_accrued, 
              'installment_principal_accrued': installment_principal_accrued, 
              'installment_interest_accrued': installment_interest_accrued 
            }
            
            #print("Terms: {0};".format(n))
            #print("Contract Principal Accrued: {0};".format(contract_principal_accrued))
            #print("Contract Interest Accrued: {0};".format(contract_interest_accrued))
            #print("installment Corpus Accrued: {0};".format(installment_corpus_accrued))
            #print("installment Principal Accrued: {0};".format(installment_principal_accrued))
            #print("installment Interest Accrued: {0};".format(installment_interest_accrued))
            
            #print("Terms: {0};".format(n))
            #print("Contract Principal Accrued: {0};".format(amounts[0]))
            #print("Contract Interest Accrued: {0};".format(amounts[1]))
            #print("installment Corpus Accrued: {0};".format(amounts[2]))
            #print("installment Principal Accrued: {0};".format(amounts[3]))
            #print("installment Interest Accrued: {0};".format(amounts[4]))
            
            print("Terms: {0};".format(amounts['term']))
            print("Contract Principal Accrued: {0};".format(amounts['contract_principal_accrued']))
            print("Contract Interest Accrued: {0};".format(amounts['contract_interest_accrued']))
            print("installment Corpus Accrued: {0};".format(amounts['installment_corpus_accrued']))
            print("installment Principal Accrued: {0};".format(amounts['installment_principal_accrued']))
            print("installment Interest Accrued: {0};".format(amounts['installment_interest_accrued']))
            
            plan.append(amounts)

            return plan
      
        plan = caculate_plan_replay(n-1, terms, principal, rateMonthly)
        last_contract_principal_accrued = plan[-1]['contract_principal_accrued']
        installment_corpus_accrued = caculate_installment_corpus_accrued(principal, terms, rateMonthly)
        installment_interest_accrued = (principal - last_contract_principal_accrued) * 1 *  rateMonthly
        installment_principal_accrued = installment_corpus_accrued - installment_interest_accrued
        contract_principal_accrued = last_contract_principal_accrued + installment_principal_accrued
        contract_interest_accrued = installment_corpus_accrued * n - contract_principal_accrued
        
        #amounts = [ 
        #   contract_principal_accrued, 
        #   contract_interest_accrued, 
        #   installment_corpus_accrued, 
        #   installment_principal_accrued, 
        #   installment_interest_accrued 
        #]
        amounts = {
          'term': n,
          'contract_principal_accrued': contract_principal_accrued, 
          'contract_interest_accrued': contract_interest_accrued, 
          'installment_corpus_accrued': installment_corpus_accrued, 
          'installment_principal_accrued': installment_principal_accrued, 
          'installment_interest_accrued': installment_interest_accrued 
        }
        
        #print("Terms: {0};".format(n))
        #print("Contract Principal Accrued: {0};".format(contract_principal_accrued))
        #print("Contract Interest Accrued: {0};".format(contract_interest_accrued))
        #print("installment Corpus Accrued: {0};".format(installment_corpus_accrued))
        #print("installment Principal Accrued: {0};".format(installment_principal_accrued))
        #print("installment Interest Accrued: {0};".format(installment_interest_accrued))
        
        #print("Terms: {0};".format(n))
        #print("Contract Principal Accrued: {0};".format(amounts[0]))
        #print("Contract Interest Accrued: {0};".format(amounts[1]))
        #print("installment Corpus Accrued: {0};".format(amounts[2]))
        #print("installment Principal Accrued: {0};".format(amounts[3]))
        #print("installment Interest Accrued: {0};".format(amounts[4]))
        
        print("Terms: {0};".format(amounts['term']))
        print("Contract Principal Accrued: {0};".format(amounts['contract_principal_accrued']))
        print("Contract Interest Accrued: {0};".format(amounts['contract_interest_accrued']))
        print("installment Corpus Accrued: {0};".format(amounts['installment_corpus_accrued']))
        print("installment Principal Accrued: {0};".format(amounts['installment_principal_accrued']))
        print("installment Interest Accrued: {0};".format(amounts['installment_interest_accrued']))
        
        plan.append(amounts)
        
        return plan

if __name__ == "__main__":
    pass
    
        